<?php
# ***** BEGIN LICENSE BLOCK *****
# This file is part of "myWiWall".
# Copyright (c) 2007-2008 CRP Henri Tudor and contributors.
# All rights reserved.
#
# "myWiWall" is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 as published by
# the Free Software Foundation.
# 
# "myWiWall" is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with "myWiWall"; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# ***** END LICENSE BLOCK *****
class HttpProxy extends AbstractHttpProxy
{
	private $requestSocket 	= null;
	private $debugFp 		= null;
	private $debug 			= false;
	
	const HEADER_LINE_MAX_LENGTH 	= 4096;
	const BODY_LINE_MAX_LENGTH 		= 8092;
	const USER_AGENT				= 'HttpProxy 1.0';
	const TRANSPORT_TCP				= 'tcp';
	const TRANSPORT_SSL				= 'ssl';

	public function __construct($requestMethod, 
								$requestHost, 
								$requestPort = 80, 
								$requestPath = '/', 
								$requestVersion = self::VERSION_11, 
								$requestTimeout = 0)
	{
		parent::__construct($requestMethod, $requestHost, $requestPort, $requestPath, $requestVersion, $requestTimeout);
		
		$this->setRequestHeader('User-agent', 	self::USER_AGENT);
		$this->setRequestHeader('Host', 		$this->requestHost);
		
		// For a performance issue, Connection: close is used by default. However, it might be modified by the user bz setting
		// its own Connection header using HttpProxy::setRequestHeader.
		$this->setRequestHeader('Connection',	'close');
	}
	
	public function __destruct()
	{
		if ($this->debug && $this->debugFp)
			@fclose($this->debugFp);
	}
	
	public function debug($debugFilePath)
	{	
		if (!$this->debugFp)
		{
			$this->debugFp = @fopen($debugFilePath, 'a+');
			$this->debug = true;
			
			$this->log('-> Debugging session for a new request.');
			$this->log('====================================', false);
		}
	}
	
	protected function checkMethod($method)
	{
		if ($method != AbstractHttpProxy::METHOD_GET &&
			$method != AbstractHttpProxy::METHOD_POST &&
			$method != AbstractHttpProxy::METHOD_PUT &&
			$method != AbstractHttpProxy::METHOD_DELETE)
			throw new HttpProxyException("Method '${method}' is not supported by MyWiWallHttpProxy. GET, POST, DELETE and PUT method are supported.", HttpProxyException::BAD_METHOD);
	}
	
	protected function checkVersion($version)
	{
		if ($version != AbstractHttpProxy::VERSION_11)
			throw new HttpProxyException("The HTTP version '${version}' is not valid. HttpProxy only supports HTTP 1.1 because of ... because !", HttpProxyException::BAD_VERSION);
	}
	
	protected function connect()
	{
		$transport = ($this->useHttps) ? self::TRANSPORT_SSL : self::TRANSPORT_TCP;
	
		// Debug
		$this->log('-> Opening socket for host=' . $this->requestHost . '; port=' . $this->requestPort . '.');

		$this->requestSocket = @fsockopen($transport . '://' . $this->requestHost,
							  			 $this->requestPort,
										 $errno,
										 $errstr,
										 $this->requestTimeout);
										 
		if (!$this->requestSocket)
			throw new HttpProxyException("Socket error when connecting. Error : ${errno} - ${errstr}.", HttpProxyException::NETWORK_ERROR);

			
		$this->log('-> Socket open.');
	}
	
	protected function disconnect()
	{

		$this->log('-> Closing socket.');
		
		if ($this->requestSocket)
		{
			@fclose($this->requestSocket);
			$this->log('-> Socket closed.');
		}
	}
	
	protected function handleRequestResponse()
	{
		// Request handling
		$path = $this->requestPath;
		if (count($this->parameters))
		{
			$path .= '?';
			foreach ($this->parameters as $param)
			{
				$path .= $param['name'] . '=' . $param['value'] . '&';
			}
			
			$path = substr($path, 0, strlen($path) - 1);
		}
		
		// General header management.
		$headerStr = $this->requestMethod . ' ' . $path . ' ' . $this->requestVersion . "\r\n";
		
		foreach ($this->requestHeaders as $fieldName => $fieldValue)
		{
			$headerStr .= $fieldName . ': ' . $fieldValue . "\r\n";
		}
		
		// Debug
		$this->log('Request headers:', false);
		$this->log('-----------------------------------------', false);
		$this->log($headerStr, false);
		
		$body = $this->requestBody;
		
		// Debug
		$this->log('Request body:', false);
		$this->log('-----------------------------------------', false);
		$this->log($body, false);
		
		$request = $headerStr . "\r\n" . $body;
		
		// Debug
		$this->log('-> Sending request.');
		
		if (!@fwrite($this->requestSocket, $request, strlen($request)))
		{
			throw new HttpProxyException('Unable to transmit HTTP Request\'s content to ' . $this->requestHost . '.', 
										 HttpProxyException::NETWORK_ERROR);
		}
		
		// Debug
		$this->log('-> Request sent.');
		
		// -- Response handling
		
		$tmpStatus = '';				// Will contain the HTTP status code.
		$statusFound = false;			// Flag to determine if the status code is already discovered or not.
		$contentLength = null;			// Will contain the Content-length header's field-value. Remains to null if not specified.
		$transferEncoding = null;		// Will contain the Transfer-encoding header's field-value. Remains to null if not specified.
		$hasResponseBody = true;		// Flag to determine if the response contains a body. 1xx, 204, 304 and any response to a HEAD request does not have a body.
		
		// Response headers handling
		while (!feof($this->requestSocket))
		{
			if (!($header = fgets($this->requestSocket, self::HEADER_LINE_MAX_LENGTH)))
			{
				//throw new HttpProxyException('Unable to retrieve HTTP Headers from ' . $this->requestHost . '.',
											// HttpProxyException::NETWORK_ERROR);
			}
		
			// Debug
			$this->log('-> ' . strlen($header) . ' bytes read from response headers.');
		
			// End of headers ?
			if (preg_match("/^\r?\n$/", $header))
				break;
			
			// What is the status ?
			if (!$statusFound && preg_match("|^HTTP/[^\s]*\s(.*?)\s|",$header, $status))
			{
				$statusFound = true;
				$tmpStatus = $status[1];
				$this->responseHttpStatus = $tmpStatus;
				
				// If code is 1xx, 204 or 304, we simply break because those kinds of messages never have message bodies.
				if ($tmpStatus == '204' || $tmpStatus == '304' || substr($tmpStatus, 0, 1) == '1')
				{
					$hasResponseBody = false;
				}
			}
			
			//  Is that the Content-length header ?
			if (!$contentLength && preg_match("/content-length: ([0-9]+)/i", $header, $cLength))
			{
				$contentLength = intval($cLength[1]);
				
				// Debug
				$this->log('-> Content length of the response will be ' . $contentLength . '.');
			}
			
			
			// We only relay headers set in $this->relayedHeaders
			$tmpResponseHeader = str_replace(array("\r", "\n"), '', $header);
			$h = explode(': ', $tmpResponseHeader);
			
			if (in_array(strtolower($h[0]), $this->relayedHeaders)) 
			{
				$this->setResponseHeader(strtolower($h[0]), $h[1]);
			}
		}
		
		// Debug
		$this->log('Response headers:', false);
		$this->log('-----------------------------------------', false);
		$this->log('HTTP ' . $tmpStatus, false);
			
		$debugResponseHeaders = '';
		foreach ($this->responseHeaders as $fieldName => $fieldValue)
		{
			$debugResponseHeaders .= $fieldName . ': ' . $fieldValue . "\r\n";
		}
			
		$this->log($debugResponseHeaders, false);
		
		// Response body handling
		if ($hasResponseBody)
		{
			$body = '';
			$readBytes = 0;
			
			// Debug
			$this->log('-> Beginning to read response body.');
				
			while (!feof($this->requestSocket))
			{
				$bodyLine = fread($this->requestSocket, self::BODY_LINE_MAX_LENGTH);
				
				$readBytes += strlen($bodyLine);
			
				// Debug
				$this->log('-> ' . strlen($bodyLine) . ' bytes read from response body.');
					
				$body .= $bodyLine;
				
				// Trying to handle the end of the response body in order to go quickly to disconnection.
				// c.f. http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.4
				if ($contentLength)
				{
					// HTTP 1.1 - 4.4 Messages - Case 3.
					if ($readBytes >= $contentLength)
						break;
				}
			}
			
			
			
			// Debug
			$this->log('-> End of response reading.');
			$this->log('Response body:', false);
			$this->log('-----------------------------------------', false);
			$this->log($body, false);
				
			$this->setResponseBody($body);
		}
	}
	
	private function log($str, $withDate = true)
	{
		if ($this->debug)
		{
			if (!$withDate)
				$str .= "\r\n";
			else
				$str = '[' . HttpProxyUtil::uDate('H:i:s.u') . '] ' . $str . "\r\n";
				
			@fwrite($this->debugFp, $str, strlen($str));
		}
	}
}
?>